package lamerman.lib.filesystem.rms;

import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import javax.microedition.rms.RecordStoreException;
import org.json.me.JSONException;
import lamerman.lib.filesystem.rms.misc.Cluster;
import java.util.Vector;
import javax.microedition.rms.RecordStore;
import net.sf.microlog.core.Logger;
import net.sf.microlog.core.LoggerFactory;
import org.json.me.JSONArray;
import org.json.me.JSONObject;
import lamerman.lib.common.CommonLib;

/**Basic class to provide filesystem*/
public class RMSFSBase {

    /**The name of RS for the file system*/
    static final String FILE_STRUCTURE_NAME = "r_fs";
    /**The name of RS for the disk space*/
    static final String FILE_SPACE_NAME = "r_space";
    /**Maximum number of fails when FS is recreated*/
    static final int MAX_FAILS = 5;
    /**The names of FS params*/
    private static final String P_NUM_OF_RSTORES = "num_rstores";
    private static final String P_NUM_OF_RSETS = "num_rsets";
    private static final String P_REC_LENGTH = "rec_length";
    private static final String P_FAILS = "num_fails";
    /**Maximum number of record stores used by the fs*/
    public int numberOfRecordStores;
    /**Maximum number of records in a record store*/
    public int numberOfRecordSets;
    /**Maximum length of a record in record store*/
    public int recordLength;
    /**Array contains information about the load of evey record store,
     * that is the number of record sets being used in a record store*/
    private int[] aRecStoreUse;
    /**Array of vectors with free clusters*/
    private Vector[] avFreeClusters;
    /**Free space left*/
    private int freeClustersLeft;
    /**Root directory*/
    private RMSFSDir root;
    /**Number of IO fails in a row, if some operation fails, it increases this
    variable, if variable reaches it's maximum value - FS reinitializes*/
    int failsInARow = 0;
    Logger log;

    private void initFields() {
        freeClustersLeft = numberOfRecordStores * numberOfRecordSets;

        avFreeClusters = new Vector[numberOfRecordStores];
        for (int rsID = 0; rsID < avFreeClusters.length; rsID++) {
            avFreeClusters[rsID] = new Vector(numberOfRecordSets);
        }

        /**Fills the vector of free space with the pointers to clusters*/
        for (int rsID = 0; rsID < numberOfRecordStores; rsID++) {
            for (int recID = 1; recID <= numberOfRecordSets; recID++) {
                avFreeClusters[rsID].addElement(new Cluster(rsID, recID));
            }
        }

        /**Initializes the aRecStoreUse array with 0*/
        aRecStoreUse = new int[numberOfRecordStores];
        for (int rsID = 0; rsID < aRecStoreUse.length; rsID++) {
            aRecStoreUse[rsID] = 0;
        }
    }

    private void deleteAllRS() {
        try {
            String[] aRS = RecordStore.listRecordStores();
            if (aRS == null) {
                return;
            }

            for (int i = 0; i < aRS.length; i++) {
                String string = aRS[i];
                if (string.indexOf(FILE_SPACE_NAME) != -1 || string.indexOf(FILE_STRUCTURE_NAME) != -1) {
                    RecordStore.deleteRecordStore(string);
                }
            }
        } catch (RecordStoreException ex) {
            ex.printStackTrace();
        }
    }

    private void testRMSCapability() {

        try {
            int maxNumOfRStores = 1;
            int maxCapOfRStore = 5000;
            int maxNumOfRSets = 1;
            int maxCapOfRSet = 5000;

            //tests for the maximum number of record stores
            deleteAllRS();
            for (int counter = 1; counter <= numberOfRecordStores; counter++) {
                RecordStore rs = null;
                try {
                    rs = RecordStore.openRecordStore(FILE_SPACE_NAME + counter, true);
                    rs.addRecord(new byte[1000], 0, 1000);
                    rs.closeRecordStore();
                    maxNumOfRStores = counter;
                } catch (Exception ex) {
                    try {
                        rs.closeRecordStore();
                        maxNumOfRStores = counter - 1;
                    } catch (RecordStoreException ex1) {
                        ex1.printStackTrace();
                    }
                    break;
                }
            }


            //max capacity of record store
            RecordStore rs = null;
            deleteAllRS();
            int step = 10000;
            int passes = ((numberOfRecordSets * recordLength) + step - 1) / step;
            for (int counter = 1; counter <= passes; counter++) {
                try {
                    rs = RecordStore.openRecordStore(FILE_SPACE_NAME, true);
                    rs.addRecord(new byte[step], 0, step);
                    rs.closeRecordStore();
                    maxCapOfRStore = counter * step;
                } catch (Exception ex) {
                    try {
                        rs.closeRecordStore();
                        maxCapOfRStore = (counter - 1) * step;
                    } catch (RecordStoreException ex1) {
                        ex1.printStackTrace();
                    }
                    break;
                }
            }

            //max number of record sets
            rs = null;
            deleteAllRS();
            for (int counter = 1; counter <= numberOfRecordSets; counter++) {
                try {
                    rs = RecordStore.openRecordStore(FILE_SPACE_NAME, true);
                    rs.addRecord(new byte[10], 0, 10);
                    rs.closeRecordStore();
                    maxNumOfRSets = counter;
                } catch (Exception ex) {

                    try {
                        rs.closeRecordStore();
                        maxNumOfRSets = counter - 1;
                    } catch (RecordStoreException ex1) {
                        ex1.printStackTrace();
                    }
                    break;
                }
            }

            //max capacity of a record set
            rs = null;
            deleteAllRS();
            step = 5000;
            passes = (recordLength + step - 1) / step;
            boolean rSetCreated = false;
            for (int counter = 1; counter <= passes; counter++) {
                try {
                    rs = RecordStore.openRecordStore(FILE_SPACE_NAME, true);

                    if (!rSetCreated) {
                        rs.addRecord(new byte[counter * step], 0, counter * step);
                    } else {
                        rs.setRecord(1, new byte[counter * step], 0, counter * step);
                    }

                    rs.closeRecordStore();
                    maxCapOfRSet = counter * step;
                } catch (Exception ex) {

                    try {
                        rs.closeRecordStore();
                        maxCapOfRSet = (counter - 1) * step;
                    } catch (RecordStoreException ex1) {
                        ex1.printStackTrace();
                    }
                    break;
                }
            }

            deleteAllRS();

            //corrects filesystem parameters
            if (numberOfRecordStores > maxNumOfRStores) {
                numberOfRecordStores = maxNumOfRStores;
            }
            if (numberOfRecordSets > maxNumOfRSets) {
                numberOfRecordSets = maxNumOfRSets;
            }
            if (numberOfRecordSets * recordLength > maxCapOfRStore) {
                int tNumOfRSets = maxCapOfRStore / recordLength;
                numberOfRecordSets = Math.min(tNumOfRSets, numberOfRecordSets);
            }
            if (recordLength > maxCapOfRSet) {
                recordLength = Math.min(maxCapOfRSet, recordLength);
            }

        } catch (Exception e) {
        }
    }

    /**Initializes filesystem when it just creates*/
    private void initFS(boolean argForceNewFS) {

        log.debug("[RMSFSBase] initializing filesystem");

        if (argForceNewFS) {
            deleteAllRS();
            testRMSCapability();
            initFields();
            root = new RMSFSDir(null, "root", this);
        } else {
            try {
                restoreFs();
            } catch (Exception ex) {
                ex.printStackTrace();

                //if some error happens during FS load, just create a new FS
                initFS(true);
            }
        }




    }

    /**The method is recursivelly called over the file structure to fill a JSONObject
    with data representing it
    @param argJson json object representing current directory
    @param argDir current directory being processed*/
    private void jsonChildren(JSONObject argJson, RMSFSDir argDir) throws JSONException {
        RMSFSNode[] aNodes = argDir.listNodes();
        //for each node in the directory
        for (int nodeID = 0; nodeID < aNodes.length; nodeID++) {
            RMSFSNode node = aNodes[nodeID];
            if (node instanceof RMSFSFile) {
                //if the node is a file
                JSONObject jsClusters = new JSONObject();
                RMSFSFile file = (RMSFSFile) node;
                //enumerates all file clusters
                for (int clID = 0; clID < file.aClusters.length; clID++) {
                    Cluster cluster = file.aClusters[clID];
                    JSONArray jsArray = new JSONArray();
                    jsArray.put(cluster.rStoreID);
                    jsArray.put(cluster.recordID);
                    jsArray.put(cluster.realLength);
                    jsClusters.put(String.valueOf(clID), jsArray);
                }
                //finally the file is added to the argJson
                argJson.put("f_" + node.name, jsClusters);
            } else if (node instanceof RMSFSDir) {
                //if the node is a directory
                JSONObject ljson = new JSONObject();
                //recursivelly calls this method for all childs
                jsonChildren(ljson, (RMSFSDir) node);
                //adds directory to the argJson
                argJson.put("d_" + node.name, ljson);
            }

        }
    }

    /**Saves file structure to rms*/
    public final synchronized void saveFs() throws JSONException, RMSFSException {
        //this part creates a JSONObject that reflects the FileSystem
        JSONObject jsRoot = new JSONObject();

        byte[] aBytes = null;

        //saves all filesystem params first
        jsRoot.put(P_NUM_OF_RSTORES, numberOfRecordStores);
        jsRoot.put(P_NUM_OF_RSETS, numberOfRecordSets);
        jsRoot.put(P_REC_LENGTH, recordLength);
        jsRoot.put(P_FAILS, failsInARow);

        jsonChildren(jsRoot, root);
        try {
            aBytes = jsRoot.toString().getBytes("utf-8");
        } catch (UnsupportedEncodingException ex) {
            throw new RMSFSException("UnsupportedEncodingException");
        }

        //saves string representation of the JSONObject to RMS
        RecordStore rs = null;
        try {
            rs = RecordStore.openRecordStore(FILE_STRUCTURE_NAME, true);
            if (rs.getNumRecords() == 0) {
                rs.addRecord(aBytes, 0, aBytes.length);
            } else {
                rs.setRecord(1, aBytes, 0, aBytes.length);
            }
            rs.closeRecordStore();
        } catch (RecordStoreException ex) {
            if (rs != null) {
                try {
                    rs.closeRecordStore();
                } catch (RecordStoreException ex1) {
                    ex1.printStackTrace();
                }
            }
            throw new RMSFSException(ex.getMessage());
        }

    }

    /**Recursive method, restores the file structure for argDir from argJson
    @param argJson json object representing directory being processed
    @param argDir directory being processed*/
    private void unjsonChildren(JSONObject argJson, RMSFSDir argDir) throws JSONException, RMSFSException, IllegalArgumentException, RMSFSFatalException {
        RMSFSNode[] aNodes = argDir.listNodes();

        //the enumeration returns all nodes from the directory
        Enumeration en = argJson.keys();
        while (en.hasMoreElements()) {
            String key = (String) en.nextElement();

            JSONObject json = (JSONObject) argJson.get(key);

            if (key.startsWith("f_")) {
                //file
                Enumeration enSectors = json.keys();
                Cluster[] aClusters = new Cluster[json.length()];

                int fileLength = 0;
                while (enSectors.hasMoreElements()) {
                    //restores information about file clusters
                    String clusterID = (String) enSectors.nextElement();
                    JSONArray jsSector = (JSONArray) json.get(clusterID);
                    int rStoreID = Integer.parseInt(jsSector.getString(0));
                    int rSetID = Integer.parseInt(jsSector.getString(1));
                    int realLength = Integer.parseInt(jsSector.getString(2));

                    int iClID = Integer.parseInt(clusterID);
                    aClusters[iClID] = new Cluster(rStoreID, rSetID);

                    //we just created a cluster, need to change free cluster pool vars
                    aRecStoreUse[rStoreID]++;

                    //decreases the number of clusters left
                    freeClustersLeft--;

                    //removes a cluster from the free cluster pool
                    int vSize = avFreeClusters[rStoreID].size();
                    for (int recID = 0; recID < vSize; recID++) {
                        avFreeClusters[rStoreID].elementAt(recID);
                        Cluster cluster = (Cluster) avFreeClusters[rStoreID].elementAt(recID);
                        if (cluster.recordID == rSetID) {
                            avFreeClusters[rStoreID].removeElementAt(recID);
                            break;
                        }
                    }

                    aClusters[iClID].realLength = realLength;
                    fileLength += realLength;
                }

                //creates a file that was just read from the json object
                RMSFSFile rmsFile = argDir.makeFile(key.substring(2), aClusters, fileLength);


            } else {
                //directory
                RMSFSDir dir = argDir.makeDir(key.substring(2));
                unjsonChildren(json, dir);
            }

        }

    }

    /**Restores the FS structure from rms*/
    private final void restoreFs() throws JSONException, RMSFSException, IllegalArgumentException, RMSFSFatalException {
        log.debug("[RMSFSBase] restoreFs attempt to restore filesystem");

        //let's get the bytes of json object first
        RecordStore rs = null;
        byte[] aBytes;
        try {
            rs = RecordStore.openRecordStore(FILE_STRUCTURE_NAME, false);
            aBytes = rs.getRecord(1);
            rs.closeRecordStore();
        } catch (RecordStoreException ex) {
            log.debug("[RMSFSBase] restoreFs RecordStoreException " + ex.getMessage());
            if (rs != null) {
                try {
                    rs.closeRecordStore();
                } catch (RecordStoreException ex1) {
                    ex1.printStackTrace();
                }
            }
            throw new RMSFSException(ex.getMessage());
        } catch (Throwable b){
            log.debug("[RMSFSBase] restoreFs bbb");
            throw new RMSFSException("bgg");
        }
        log.debug("[RMSFSBase] restoreFs aaa");
        String fs;
        try {
            fs = new String(aBytes, "utf-8");
        } catch (UnsupportedEncodingException ex) {
            throw new RMSFSException("UnsupportedEncodingException");
        }

        //creates the json object

        JSONObject jsRoot = new JSONObject(fs);

        //restores the FS parameters first
        numberOfRecordStores = jsRoot.getInt(P_NUM_OF_RSTORES);
        jsRoot.remove(P_NUM_OF_RSTORES);

        numberOfRecordSets = jsRoot.getInt(P_NUM_OF_RSETS);
        jsRoot.remove(P_NUM_OF_RSETS);

        recordLength = jsRoot.getInt(P_REC_LENGTH);
        jsRoot.remove(P_REC_LENGTH);

        failsInARow = jsRoot.getInt(P_FAILS);
        jsRoot.remove(P_FAILS);

        if (log.isDebugEnabled()){
            log.debug("[RMSFSBase] restoreFs num RStores " + numberOfRecordStores + " RSets " + numberOfRecordSets + " RLength " + recordLength + " fails " + failsInARow);
        }

        initFields();

        RMSFSDir lRoot = new RMSFSDir(null, "root", this);

        //and calls the unjsonChildren method
        unjsonChildren(jsRoot, lRoot);

        log.debug("[RMSFSBase] restoreFs the FS has been initialized");

        root = lRoot;

    }

    /**Sorts an array of clusters with bubblesort*/
    private void bubblesort(Cluster[] arr) {
        for (int i = arr.length - 1; i >= 0; i--) {
            for (int j = 0; j < i; j++) {
                if (arr[j].rStoreID > arr[j + 1].rStoreID) {
                    Cluster t = arr[i];
                    arr[i] = arr[j];
                    arr[j] = t;
                }
            }
        }
    }

    /**Allocates clusters for new files
    @param argSize size to allocate
    @return array of clusters that was just allocated*/
    final Cluster[] allocateSpace(int argSize) throws RMSFSException, RMSFSFatalException {
        //if the number of records left is less than required
        if (log.isDebugEnabled()) {
            log.debug("[RMSFS] allocateSpace requested size : " + argSize + " , free space left: " + freeClustersLeft * recordLength);
        }
        Cluster[] result = null;


        if (argSize > freeClustersLeft * recordLength) {
            throw new RMSFSException("There is not enough space in the record store");
        } else {
            try {
                int recordsRequired = (argSize + recordLength - 1) / recordLength;
                result = new Cluster[recordsRequired];

                for (int clID = 0; clID < recordsRequired; clID++) {

                    //chooses the record store with the lowest index
                    int lowestRS = -1;
                    for (int rsID = 0; rsID < aRecStoreUse.length; rsID++) {
                        if (aRecStoreUse[rsID] != numberOfRecordSets) {
                            lowestRS = rsID;
                        }
                    }

                    /**The elements in the Vector are always sorted, so the lowest
                    recordID is always taken*/
                    result[clID] = (Cluster) avFreeClusters[lowestRS].firstElement();
                    avFreeClusters[lowestRS].removeElementAt(0);

                    //increases the number of records used in the RS
                    aRecStoreUse[result[clID].rStoreID]++;
                    //decreases overall number of free clusters
                    freeClustersLeft--;


                }

                if (log.isDebugEnabled()) {
                    log.debug("[RMSFS] allocateSpace Free space: " + freeClustersLeft * recordLength);
                }

                bubblesort(result);
            } catch (Exception e) {
                e.printStackTrace();
                throw new RMSFSFatalException("RMSFSFatalException at allocateSpace");
            }
        }


        return result;

    }

    /**Releases used clusters as they get free
    @param argClusters clusters to be deallocated*/
    final void deallocateSpace(Cluster[] argClusters) throws RMSFSFatalException {
        try {
            for (int clID = 0; clID < argClusters.length; clID++) {
                int rStoreId = argClusters[clID].rStoreID;
                //decreases the number of records used in the RS
                aRecStoreUse[rStoreId]--;

                //finds position to insert the element, since the vector should be sorted
                int posToInsert = 0;
                for (; posToInsert < avFreeClusters[rStoreId].size(); posToInsert++) {
                    Cluster cluster = (Cluster) avFreeClusters[rStoreId].elementAt(posToInsert);
                    if (cluster.recordID > argClusters[clID].recordID) {
                        break;
                    }
                }

                avFreeClusters[rStoreId].insertElementAt(argClusters[clID], posToInsert);
                freeClustersLeft++;

                //System.out.println("Deallocated " + rStoreId + "," + argClusters[clID].recordID);

                //if noone is using the RS, delete it
                if (aRecStoreUse[rStoreId] == 0) {
                    if (log.isDebugEnabled()) {
                        log.debug("[RMSFS] deallocateSpace Record store " + rStoreId + " was deleted");
                    }

                    try {
                        RecordStore.deleteRecordStore(RMSFSBase.FILE_SPACE_NAME + rStoreId);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new RMSFSFatalException("RMSFSFatalException at deallocateSpace");
        }
    }

    /**Returns a node by path
    @param argPath absolute path to the node
    @return RMSFSNode node the address is pointing to*/
    public final synchronized RMSFSNode getNode(String argPath) throws RMSFSException, IllegalArgumentException {
        RMSFSDir curNode = root;

        String[] nodes = CommonLib.split(argPath, "/");
        for (int nodeID = 0; nodeID < nodes.length - 1; nodeID++) {
            String sNode = nodes[nodeID];
            //user might write the path beginning with /
            if (nodeID == 0 && sNode.length() == 0) {
                continue;
            }

            curNode = (RMSFSDir) curNode.getNode(sNode);
        }

        String tNode = nodes[nodes.length - 1];
        //user may write a "/" symbol in the end of path for directories
        //like this "/usr/bin/"
        if (tNode.length() == 0) {
            return curNode;
        } else {
            return curNode.getNode(tNode);
        }


    }

    /**Returns the filesystem root
    @return RMSFSDir filesystem root*/
    public final RMSFSDir getRoot() {
        return root;
    }

    /**Creates a new filesystem
     @param argMaxNumberOfRecordStores maximum number of record stores
     @param argNumberOfRecords number of record sets in a record store
     @param argRecordLength the length of a record in record set
     @param argForceNewFS should it use existing fs in the rms or force a new one,
     * deleting the old one if it exists*/
    public RMSFSBase(int argMaxNumberOfRecordStores, int argNumberOfRecords, int argRecordLength, boolean argForceNewFS) {
        numberOfRecordStores = argMaxNumberOfRecordStores;
        numberOfRecordSets = argNumberOfRecords;
        recordLength = argRecordLength;

        log = LoggerFactory.getLogger("default");

        initFS(argForceNewFS);
    }
}
